fix(grammar): stop the Swift try-bang entry shifting past its own type - #1986
Conversation
|
Thanks for opening this — it has been seen, and it is queued. This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence. Current review status: working through a backlog. What that means for this PR, concretely:
Things that will genuinely speed it up whenever review does happen:
If this fixes a bug, a reproduction we can run is worth more than a description of the symptom. Thanks for contributing, and sorry in advance for the wait. |
|
Good catch, and the fix is right. One piece of paperwork is missing before it can land. Please add a row to the "Local source patches" table in You are in good company: three of the four existing rows are the same class of UBSan fix in a vendored scanner. Suggested row, matching the existing shape: Worth upstreaming too, if you have the appetite — On the change itself, two things I want to name because they are the reason this was easy to review: You found a bug that the normal test build actively hides. Recovering UBSan prints the diagnostic and continues, so the shift has been executing and being ignored for as long as it has existed. Identifying that the Windows CLANGARM64 leg runs UBSan in trap mode — where the same shift is an illegal instruction rather than a log line — is what turns "a warning nobody reads" into "a crash on one platform". And you wrote the test's own limitation into the test. "This test cannot go red here… parsing this file at all is the check" is exactly the right comment to leave. A future reader who sees it pass on macOS will not mistake that for evidence. That honesty is worth more than the four-character fix. Add the manifest row and this is ready. |
The Swift scanner keeps a 64-bit mask of the symbols that suppress a
match -- the rule that stops `try!` emitting its `!` as a token of its
own. It tests one bit per candidate:
uint64_t suppressing_symbols = OP_SYMBOL_SUPPRESSOR[full_match];
for (uint64_t suppressor = 0; suppressor < TOKEN_COUNT; suppressor++) {
if (!(suppressing_symbols & 1 << suppressor)) {
The mask is uint64_t but the literal `1` is an int, so the shift is an
int shift. TOKEN_COUNT is larger than 32, so once suppressor reaches 31
the shift runs past the width of the type. That is undefined behavior,
and every bit above 31 is tested against a value the standard does not
define.
Nothing caught it because nothing in the tree reached the suppressor
path. Any Swift force-unwrap does: `cached!` is enough.
UBSan reports it as:
scanner.c:514:47: runtime error: left shift of 1 by 31 places
cannot be represented in type 'int'
`1ULL` makes the literal as wide as the mask it is tested against.
The new test in tests/test_extraction.c cannot go red on its own. The
normal test build prints the UBSan message and carries on, which is why
this survived. The Windows CLANGARM64 leg runs UBSan in trap mode, and
there the same shift is an illegal-instruction crash -- so the test
exists to make sure that leg keeps parsing a force-unwrap at all.
scripts/vendored-checksums.txt records the new hash for the one changed
file, as scripts/security-vendored.sh --update writes it. Layer 8 of the
security gate compares vendored content against that manifest, so the
edit and its recorded hash belong in the same commit.
Found while adding Swift URL extraction in DeusData#1892 / DeusData#1976, and split out
of that PR so the vendored change can be reviewed on its own.
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
scanner.c line 131 shifts a literal 1UL by 32 to build the entry that suppresses the bang of a try!. unsigned long is 64 bits on Linux and macOS but 32 bits on Windows, so there the shift count equals the width of the type, which is undefined. 1ULL is 64 bits on every target this project builds for. Nothing reports it today. The expression is a compile-time constant, so no sanitizer sees it run, and Makefile.cbm:719 builds vendored grammars with -w, which switches off -Wshift-count-overflow. Upstream tree-sitter-swift made the same change in 6ab8d1d74ebd, after the commit this grammar is pinned to. Refs DeusData#1892 Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
26d71fc to
b2ac3c5
Compare
|
Manifest row added, and the branch is rebased onto current The rowI used your shape and extended the reason to name both sites, since this PR fixes two: The commit touches a second file, and that is deliberate rather than scope creep. The rebase, and what the conflict wasThe branch was 44 commits behind and conflicting. The conflict was one line, and it was that same self-reference: I resolved it by recomputing the hash from the merged file rather than taking either side. Both recorded values described a
UpstreamingYes, and I have not done it yet — I will open it against On the two things you namedThe UBSan point is the one I want to be honest about: I did not identify the trap-mode difference from insight. I hit the recovering-UBSan diagnostic while running an unrelated test, went looking for which leg treats it as fatal, and found the CLANGARM64 one. The finding was real; the route to it was ordinary. The test comment was deliberate. A test that cannot fail on the machine most people run it on is worth less than it looks, and writing that down felt cheaper than having someone later mistake a green macOS run for coverage. |
The vendoring table claims our copy of a grammar matches the pinned upstream commit. A local patch makes that claim false unless the patch is written down, so a future re-vendor would drop the fix without anyone noticing. The row covers both changed lines, because both are the same defect in the same file: OP_SYMBOL_SUPPRESSOR at line 131 and eat_operators at line 514. The row also names the two upstream commits that already carry these changes, and the table intro gains one clause for the case. Upstream fixed eat_operators in fb63a7004f07 on 2026-04-06 (their issue DeusData#558) and the OP_SYMBOL_SUPPRESSOR entry in 6ab8d1d74ebd on 2026-08-10. Our pin 8abb3e8b3325 is from 2026-03-20 and predates both, so these are backports rather than local inventions. That distinction changes what a re-vendor should do. The table's instruction is "must re-apply these", which is right for the other four rows -- upstream never took those. For this row it would mean hand-applying a change the newer file already has. The row now says to delete it instead, and the intro allows for that case. MANIFEST.md is itself listed in scripts/vendored-checksums.txt, so its own recorded checksum moves with the edit. Refs DeusData#1892 Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
b2ac3c5 to
e820409
Compare
|
The MANIFEST row is in, and I have to correct two things I said. The upstream fix is already upstream — I cannot open itI said I would open this against
Both fixes still matter here. The row I first wrote would have misled a re-vendorYour reason for wanting the row is that a re-vendor would otherwise silently drop the fix. That reason is exactly right for the other four rows, where upstream never took the change. It inverts for this one. The table says:
Anyone following that on the Swift row after a re-vendor past 2026-08-10 would hand-apply a change the newer file already has. So the row differs from your suggested wording in two ways. The reason column now names both upstream commits, our pin, and the dates, and says to delete this row on a re-vendor rather than re-apply it. The table intro gains one clause allowing that case, because it is a property of the table rather than of one row:
The What is not hereRe-vendoring the Swift grammar is the real end state and it is not in this pull request.
I amended the MANIFEST commit rather than stacking a correction on it, so the branch is still three commits. The two code commits are untouched. |
|
Merged as I asked for a And you encoded the consequence in the manifest rather than only in the row:
That turns a static instruction into a self-retiring one. Without it, a re-vendor past 2026-08-10 would dutifully re-apply a patch upstream no longer needs, and the local-patch table would accumulate entries nobody can safely remove because the reason for each has been lost. Naming the upstream commit in the row is what makes the deletion decidable later. That is a better answer than the one I proposed. The original diagnosis stands too: recovering UBSan prints and continues, which is why Before merging I confirmed the 12-commit gap to Thank you — that is your second merge today, after #2008. |
One token in the same vendored file as #1977. This is the second of the two shifts in #1978, and the one that is undefined on Windows.
Stacked on #1977, because both changes rewrite the same recorded hash in
scripts/vendored-checksums.txt. Two branches offmainwould collide there. Until #1977 merges this PR shows both commits; after it merges the diff narrows to the one token below.The bug
internal/cbm/vendored/grammars/swift/scanner.c:131builds the entry that suppresses the!of atry!:FAKE_TRY_BANGis the last member of a 33-entryTokenTypeenum, so its value is 32.unsigned longis 64 bits on Linux and macOS. On Windows it is 32 bits, so the shift count equals the width of the type, which is undefined.1ULLis 64 bits on every target this project builds for.Why nothing catches it
Three things line up, and the third is the one I would not have guessed:
Makefile.cbm:719compiles every vendored grammar with-w. That switches off-Wshift-count-overflow, the one diagnostic that names this exact defect. The comment on line 718 gives the reason: upstream code has warnings.CLANG64onwindows-latestsits inCORE_WIN(.github/workflows/_test.yml:63), so it builds on every pull request and reports nothing.What I did and did not verify
I have no Windows machine, so I did not observe a symptom in the product. What I did do is compile the same shape locally, using
unsigned intbecause it is 32 bits everywhere:cc -Wallwarnsshift count >= width of type. Adding-w, as the grammar build does, silences it.I expected the folded value to be
0and said so in #1978 before testing. It is not. The variable is simply never assigned, and three runs of one binary printed4338156640,4364321888and4377183328; at-O2it printed8447164672. The correct form,(uint64_t)1 << 32, prints4294967296every time. So the practical effect on Windows is a suppressor entry holding whatever happens to be there, not a predictable zero.No test, and why
A test here would pass before and after the change on every machine CI can run it on.
unsigned longis already 64 bits on Linux and macOS, so the expression is correct there; the defect only exists on a target the test suite does not execute. Adding a test that cannot fail would suggest a guarantee that does not exist. The existingswift_force_unwrap_scanner_shiftfrom #1977 already keeps a force-unwrap parsing at all.Upstream
Not my own reading alone.
alex-pinkus/tree-sitter-swiftmade the same one-token change in6ab8d1d74ebd(2026-08-10). Our copy is pinned at8abb3e8b3325(2026-03-20), so that fix landed after our pin. I checked our copy byte-for-byte against upstream at that pin: it is a faithful vendor, not a local edit that drifted.Re-vendoring picks up both fixes and five months of other work, but
TOKEN_COUNTmoves from 33 to 34 upstream, so the grammar itself changed.CONTRIBUTING.md:134puts a vendored dependency change behind a design discussion, so I have not proposed it here.Files
internal/cbm/vendored/grammars/swift/scanner.c1ULto1ULLon line 131scripts/vendored-checksums.txtscripts/security-vendored.sh --updatewrites itLayer 8 of the security gate compares vendored content against that manifest, so the edit and its recorded hash land together.
scripts/security-vendored.shexits 0 on this branch.One thing worth your call
-won the grammar build is what hid this. Turning on the single warning-Wshift-count-overflowfor vendored grammars — one narrow diagnostic, not-Wall -Werror— would catch this class across all 104 vendored scanners without touching the rest of the noise. I have not made that change here. Say the word and I will open it as its own issue.Fixes #1978
Refs #1892